﻿/********************************************************************************
* Copyright 2010 Zane Thorn (zane.thorn@gmail.com)                              *
*                                                                               *
* NeturalMath is free software: you can redistribute it and/or modify           *
* it under the terms of the GNU Lesser General Public License as published by   *
* the Free Software Foundation, either version 3 of the License, or             *
* (at your option) any later version.                                           *
*                                                                               *
* NeturalMath is distributed in the hope that it will be useful,                *
* but WITHOUT ANY WARRANTY; without even the implied warranty of                *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the                 *
* GNU Lesser General Public License for more details.                           *
*                                                                               *
* You should have received a copy of the GNU Lesser General Public License      *
* along with NeturalMath.  If not, see <http://www.gnu.org/licenses/>.          *
********************************************************************************/

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using NeturalMath.Properties;

namespace NeturalMath.Language
{
    public class Lexer:ITokenProvider
    {
        #region Local Members

        //private Token _token;
        private readonly TextReader _reader;

        private char _current;
        
        private const char EOF = unchecked((char)(-1));

        private static readonly Dictionary<string, TokenType> _keywords 
            = new Dictionary<string, TokenType> {
                                                    { "print", TokenType.PrintKeyword },
                                                    { "def", TokenType.DefKeyword },
                                                    { "import", TokenType.ImportKeyword },
                                                    { "true", TokenType.TrueConstant },
                                                    { "false", TokenType.FalseConstant },
                                                    { "void", TokenType.VoidConstant },
                                                    { "public", TokenType.PublicAccess },
                                                    { "private", TokenType.PrivateAccess },
                                                    { "const", TokenType.ConstantAccess },
                                                    { "readonly", TokenType.ReadOnlyAccess },
                                                    { "global", TokenType.GlobalAccess },
                                                };

        #endregion

        #region Constructors

        public Lexer(TextReader reader)
        {
            if (reader == null)
                throw new ArgumentNullException("reader");
            
            _reader = reader;
        }

        #endregion

        #region Public Methods


        public Token GetToken()
        {
            // Look at the character and decide what to do next
            _current = GetChar();
            switch (_current)
            {
                case EOF:
                    return new Token(TokenType.EOF);
                case ' ':
                case '\t':
                    // Skip whitespace
                    return GetToken();
                case '\r':
                case '\n':
                    return new Token(TokenType.NewLine);
                case '(':
                    return new Token(TokenType.GroupingBegin);
                case ')':
                    return new Token(TokenType.GroupingEnd);
                case '+':
                    return new Token(TokenType.AddOperator);
                case '-':
                    return CheckMinus();
                case '*':
                    return new Token(TokenType.MultiplyOperator);
                case '/':
                    return CheckDivide();
                case '^':
                    return new Token(TokenType.PowerOperator);
                case '&':
                    return new Token(TokenType.AndOperator);
                case '|':
                    return new Token(TokenType.OrOperator);
                case '%':
                    return new Token(TokenType.ModulusOperator);
                case '{':
                    return new Token(TokenType.DomainBegin);
                case '}':
                    return new Token(TokenType.DomainEnd);
                case '[':
                    return new Token(TokenType.SetBegin);
                case ']':
                    return new Token(TokenType.SetEnd);
                case '\'':
                    return CheckString();
                case '1':
                case '2':
                case '3':
                case '4':
                case '5':
                case '6':
                case '7':
                case '8':
                case '9':
                case '0':
                    return CheckNumber();
                case '`':
                    return new Token(TokenType.DerivitiveOperator);
                case '~':
                    return new Token(TokenType.NotOperator);
                case ',':
                    return new Token(TokenType.CommaSeperator);
                case '<':
                    return CheckLessThan();
                case '>':
                    return CheckGreaterThan();
                case '=':
                    return CheckEquals();
                case '#':
                    return new Token(TokenType.UnitOperator);
                case '$':
                    _current = GetChar();
                    var identifier = CheckIdentifierOrKeyword();
                    return new Token(TokenType.SymbolicLookup, identifier.Text);
                case '!':
                    return new Token(TokenType.FactorialOperator);

                case ':':
                case '_':
                case '@':
                case '"':
                case ';':
                case '\\':
                    throw new LexicalException(
                        ErrorCodes.UnrecognizedCharacterSequence,
                            string.Format(
                                Resources.SyntaxUnrecognizedCharacter,
                                _current
                                )
                            );
                default:
                    return CheckIdentifierOrKeyword();
            }
        }

        public void ReadToEndOfLine()
        {
            while (!IsEndOfLine(_current))
            {
                _current = GetChar();
            }
        }

        #endregion

        #region Helper Methods

        private Token CheckMinus()
        {
            var next = PeekChar();
            if (next == '>')
            {
                GetChar();
                return new Token(TokenType.RangeOperator);
            }

            return new Token(TokenType.SubtractOperator);
        }

        private Token CheckLessThan()
        {
            var next = PeekChar();
            switch (next)
            {
                case '>':
                    GetChar();
                    return new Token(TokenType.NotEqualOperator);
                case '=':
                    GetChar();
                    return new Token(TokenType.LessThanOrEqualOperator);
                default:
                    return new Token(TokenType.LessThanOperator);
            }
        }

        private Token CheckGreaterThan()
        {
            var next = PeekChar();
            switch (next)
            {
                case '<':
                    GetChar();
                    return new Token(TokenType.XorOperator);
                case '=':
                    GetChar();
                    return new Token(TokenType.GreaterThanOrEqualOperator);
                default:
                    return new Token(TokenType.GreaterThanOperator);
            }
        }

        private Token CheckEquals()
        {
            var next = PeekChar();
            switch (next)
            {
                case '=':
                    GetChar();
                    return new Token(TokenType.IsEqualOperator);
                default:
                    return new Token(TokenType.AssignmentOperator);
            }
        }

        private Token CheckDivide()
        {
            var next = PeekChar();

            if (_current == next)
            {
                ReadToEndOfLine();
                return new Token(TokenType.NewLine);
            }

            return new Token(TokenType.DivideOperator);
        }

        private Token CheckString()
        {
            var stringConst = new List<char>();

            while (true)
            {
                _current = GetChar();
                if (IsEndOfLine(_current))
                    throw new LexicalException(ErrorCodes.UnexpectedEndOfLine, Resources.LexicalUnexpectedEndOfLine);
                if (_current == '\'')
                    break;

                stringConst.Add(_current);
            }

            return new Token(TokenType.StringConstant, new String(stringConst.ToArray()));
        }

        private Token CheckNumber()
        {
            var stringConst = new List<char>();

            while (true)
            {
                stringConst.Add(_current);

                var next = PeekChar();

                if (next == '.' || char.IsDigit(next))
                {
                    _current = GetChar();
                }
                else
                    break;
            }
            return new Token(TokenType.NumericConstant, new String(stringConst.ToArray()));
        }

        private Token CheckIdentifierOrKeyword()
        {
            var stringConst = new List<char>();
            while (true)
            {
                stringConst.Add(_current);
                var next = PeekChar();

                if (char.IsLetterOrDigit(next) || next=='.')
                {
                    _current = GetChar();
                }
                else
                    break;
            }

            var identString = new String(stringConst.ToArray());

            if (_keywords.ContainsKey(identString))
                return new Token(_keywords[identString]);

            return new Token(TokenType.Identifier, identString);
        }

        private char GetChar() {
            return unchecked ((char)_reader.Read());
        }

        private char PeekChar() {
            return unchecked ((char)_reader.Peek());
        }

        private bool IsEndOfLine(char c)
        {
            return c == '\n' || c == EOF;
        }

        #endregion
    }
}
